﻿// MainForm.cs
// (c) 2011-2016, Charles Lechasseur
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using PathCopyCopy.Settings.Core;
using PathCopyCopy.Settings.Core.Base;
using PathCopyCopy.Settings.Core.Plugins;
using PathCopyCopy.Settings.Properties;
using PathCopyCopy.Settings.UI.Utils;

namespace PathCopyCopy.Settings.UI.Forms
{
    /// <summary>
    /// Main form of the Path Copy Copy settings application.
    /// </summary>
    public partial class MainForm : Form
    {
        /// Paths separator that copies multiple paths on the same line.
        private const string PATHS_SEPARATOR_ON_SAME_LINE = " ";

        /// Name of our LICENSE file.
        private const string LICENSE_TXT_FILE = "LICENSE.TXT";

        /// URI of the Donations page.
        private const string DONATIONS_PAGE_URI = @"https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=LM5B9WNTH4KN4&lc=CA&item_name=Charles%20Lechasseur&item_number=PathCopyCopy&currency_code=CAD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted";

        /// URI of the page specifying the Path Copy Copy license on CodePlex.
        private const string LICENSE_PAGE_URI = @"https://pathcopycopy.codeplex.com/license";

        /// Extension of legacy pipeline plugins export files.
        private const string LEGACY_PIPELINE_PLUGINS_EXT = ".pccpp";

        /// Path to license file, either locally or online.
        private string licensePath;

        /// Object used to access user settings.
        private UserSettings settings;
        
        /// BindingList used to store all existing plugins.
        private BindingList<Plugin> allPlugins;

        /// BindingList used to store plugins to show in main menu, in display order.
        private BindingList<Plugin> pluginsInMainMenu;

        /// BindingList used to store plugins to show in submenu, in display order.
        private BindingList<Plugin> pluginsInSubmenu;
        
        /// <summary>
        /// Constructor
        /// </summary>
        public MainForm()
        {
            InitializeComponent();

            // Set form's max width to the min width. We have to specify a height though
            // since leaving it to 0 will mean 0 in this case, so we use INT_MAX. Idea taken from
            // http://stackoverflow.com/questions/13527142/is-it-possible-to-set-the-maximum-width-for-a-form-but-leave-the-maximum-height
            // Doing this in code is easier since representing INT_MAX is awkward in the wizard.
            MaximumSize = new Size(MinimumSize.Width, Int32.MaxValue);

            // Connect all checkboxes in the Options page to the CheckChanged event handler that
            // will enable the Apply button.
            foreach (Control control in MiscOptionsPage.Controls) {
                if (control is CheckBox) {
                    (control as CheckBox).CheckedChanged += Chk_CheckedChanged;
                }
            }

            // Locate license file.
            Assembly thisAssembly = this.GetType().Assembly;
            licensePath = LocateLicenseFile(thisAssembly);

            // Init about "box" controls.
            Version version = thisAssembly.GetName().Version;
            int numComponents;
            if (version.Revision > 0) {
                numComponents = 4;
            } else if (version.Build > 0) {
                numComponents = 3;
            } else {
                numComponents = 2;
            }
            ProductAndVersionLbl.Text = String.Format(ProductAndVersionLbl.Text, version.ToString(numComponents));
            CopyrightLbl.Text = GetAssemblyCopyrightString(thisAssembly);
            MainToolTip.SetToolTip(CodePlexSiteLinkLbl, CodePlexSiteLinkLbl.Text);
            MainToolTip.SetToolTip(LicenseTxtLinkLbl, licensePath);
            DonationLinkLbl.Links[0].LinkData = DONATIONS_PAGE_URI;
            MainToolTip.SetToolTip(DonationLinkLbl, DONATIONS_PAGE_URI);
        }
        
        /// <summary>
        /// Given the reference to an <see cref="Assembly"/>, returns the copyright
        /// string as found in its <see cref="AssemblyCopyrightAttribute"/>.
        /// </summary>
        private string GetAssemblyCopyrightString(Assembly assembly)
        {
            string copyrightString = null;

            // Look for custom attribute containing copyright string.
            object[] copyrightAttributes = assembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), true);
            if (copyrightAttributes.Length != 0) {
                // Extract the entire string from the attribute.
                string entireCopyrightString = ((AssemblyCopyrightAttribute) copyrightAttributes[0]).Copyright;

                // If there is a period, stop there. We don't want the part about LICENSE.TXT.
                int periodPos = entireCopyrightString.IndexOf('.');
                if (periodPos >= 0) {
                    copyrightString = entireCopyrightString.Substring(0, periodPos);
                } else {
                    Debug.Fail("Copyright string is not formatted like we thought?");
                    copyrightString = entireCopyrightString;
                }
            } else {
                // Use default copyright.
                Debug.Fail("No copyright attribute on assembly?");
                copyrightString = "(c) 2008-2016, Charles Lechasseur";
            }

            // Replace "(c)" with the copyright symbol
            Debug.Assert(!String.IsNullOrEmpty(copyrightString));
            copyrightString = copyrightString.Replace("(c)", "\u00A9");

            return copyrightString;
        }

        /// <summary>
        /// Looks for the LICENSE.TXT file on disk and returns its full path.
        /// If not found, returns the URL of the license file online.
        /// </summary>
        /// <param name="assembly">Assembly used to locate license file on disk.</param>
        /// <returns>Path to license file, on disk or online.</returns>
        private string LocateLicenseFile(Assembly assembly)
        {
            Debug.Assert(assembly != null);

            // In a normal installation, the license file will be located right next to the executable.
            string licensePath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(assembly.Location)), LICENSE_TXT_FILE);
            FileInfo licenseFileInfo = new FileInfo(licensePath);
            if (!licenseFileInfo.Exists) {
                // In a dev environment, license file is three directories up.
                FileInfo[] fileInfos = null;
                DirectoryInfo dirInfo = licenseFileInfo.Directory;
                for (int i = 0; dirInfo != null && i < 3; ++i) {
                    dirInfo = dirInfo.Parent;
                }
                if (dirInfo != null) {
                    fileInfos = dirInfo.GetFiles(LICENSE_TXT_FILE);
                }
                if (fileInfos != null && fileInfos.Length != 0) {
                    Debug.Assert(fileInfos.Length == 1);
                    licenseFileInfo = fileInfos[0];
                } else {
                    licenseFileInfo = null;
                }
            }
            
            // If license file was found, return its full path, otherwise return online URL.
            return licenseFileInfo != null ? licenseFileInfo.FullName : LICENSE_PAGE_URI;
        }
        
        /// <summary>
        /// Loads the options and populates the form.
        /// </summary>
        private void LoadUserSettings()
        {
            Debug.Assert(settings != null);

            // Create binding list to store all plugins. Populate it with list
            // of all plugins in default order.
            allPlugins = new BindingList<Plugin>(PluginsRegistry.GetPluginsInDefaultOrder(settings));

            // Create sorted set of all plugins from the list above, to be able to perform lookups.
            SortedPluginsSet setOfAllPlugins = new SortedPluginsSet(allPlugins);

            // Create binding list to store plugins in main menu. Populate it by
            // ordering plugins using display order from settings.
            List<Guid> mainMenuDisplayOrder = settings.MainMenuDisplayOrder;
            if (mainMenuDisplayOrder == null) {
                // No main menu plugin specified in settings, use default.
                mainMenuDisplayOrder = new List<Guid>();
                mainMenuDisplayOrder.Add(new Guid(Resources.LONG_PATH_PLUGIN_ID));
            }
            pluginsInMainMenu = new BindingList<Plugin>(PluginsRegistry.OrderPluginsToDisplay(
                setOfAllPlugins, mainMenuDisplayOrder, null, allPlugins));

            // Create binding list to store plugins in submenu. Same as above, but we
            // also pass list of known plugins from settings. This way, new plugins will,
            // by default, appear at the end of the submenu.
            List<Guid> submenuDisplayOrder = settings.SubmenuDisplayOrder;
            if (submenuDisplayOrder == null) {
                // No submenu plugins specified in settings, use all plugins in default order.
                submenuDisplayOrder = new List<Guid>();
                foreach (Plugin plugin in allPlugins) {
                    submenuDisplayOrder.Add(plugin.Id);
                }
            }
            List<Guid> knownPlugins = settings.KnownPlugins;
            SortedSet<Guid> knownPluginsAsSet = null;
            if (knownPlugins != null) {
                knownPluginsAsSet = new SortedSet<Guid>(knownPlugins);
            }
            pluginsInSubmenu = new BindingList<Plugin>(PluginsRegistry.OrderPluginsToDisplay(
                setOfAllPlugins, submenuDisplayOrder, knownPluginsAsSet, allPlugins));

            // Set binding list as data source for the various plugins list views.
            AllPluginsListView.DataSource = allPlugins;
            MainMenuPluginsListView.DataSource = pluginsInMainMenu;
            SubmenuPluginsListView.DataSource = pluginsInSubmenu;
            
            // Set options checkboxes.
            AddQuotesChk.Checked = settings.AddQuotes;
            EmailLinksChk.Checked = settings.MakeEmailLinks;
            HiddenSharesChk.Checked = settings.UseHiddenShares;
            AlwaysShowSubmenuChk.Checked = settings.AlwaysShowSubmenu;
            UseIconForSubmenuChk.Checked = settings.UseIconForSubmenu;
            UsePreviewModeChk.Checked = settings.UsePreviewMode;
            DropRedundantWordsChk.Checked = settings.DropRedundantWords;
            EnableSoftwareUpdateChk.Checked = !settings.DisableSoftwareUpdate;

            // Encode param is a little special since it has two checkboxes.
            switch (settings.EncodeParam) {
                case UserSettings.StringEncodeParam.None: {
                    break;
                }
                case UserSettings.StringEncodeParam.Whitespace: {
                    EncodeURIWhitespaceChk.Checked = true;
                    break;
                }
                case UserSettings.StringEncodeParam.All: {
                    EncodeURIWhitespaceChk.Checked = true;
                    EncodeURICharsChk.Checked = true;
                    break;
                }
                default: {
                    Debug.Fail(String.Format("Unknown encode param: {0}", settings.EncodeParam));
                    EncodeURIWhitespaceChk.Enabled = false;
                    break;
                }
            }

            // "Copy on same line" is a little special since it could be any string.
            Debug.Assert(!CopyOnSameLineChk.Checked);
            string pathsSeparator = settings.PathsSeparator;
            if (pathsSeparator == PATHS_SEPARATOR_ON_SAME_LINE) {
                CopyOnSameLineChk.Checked = true;
            } else if (!String.IsNullOrEmpty(pathsSeparator)) {
                CopyOnSameLineChk.Enabled = false;
            }

            // All those changes probably enabled the "Apply" button so disable it again here.
            ApplyBtn.Enabled = false;

            Debug.Assert(allPlugins != null);
            Debug.Assert(pluginsInMainMenu != null);
            Debug.Assert(pluginsInSubmenu != null);
        }
        
        /// <summary>
        /// Saves the current options in the user settings.
        /// </summary>
        private void SaveUserSettings()
        {
            Debug.Assert(settings != null);
            Debug.Assert(allPlugins != null);
            Debug.Assert(pluginsInMainMenu != null);
            Debug.Assert(pluginsInSubmenu != null);

            // Build lists of plugin IDs to save in config for main menu and submenu.
            List<Guid> mainMenuDisplayOrder = new List<Guid>();
            foreach (Plugin plugin in pluginsInMainMenu) {
                mainMenuDisplayOrder.Add(plugin.Id);
            }
            List<Guid> submenuDisplayOrder = new List<Guid>();
            foreach (Plugin plugin in pluginsInSubmenu) {
                submenuDisplayOrder.Add(plugin.Id);
            }

            // If the only plugin left to show in the main menu is our default one,
            // clear the value instead.
            if (mainMenuDisplayOrder.Count == 1 && mainMenuDisplayOrder[0] == new Guid(Resources.LONG_PATH_PLUGIN_ID)) {
                mainMenuDisplayOrder = null;
            }

            // Save plugin display orders in config.
            settings.MainMenuDisplayOrder = mainMenuDisplayOrder;
            settings.SubmenuDisplayOrder = submenuDisplayOrder;

            // Build list of known plugin IDs.
            SortedPluginsSet setOfAllPlugins = new SortedPluginsSet(allPlugins);
            List<Guid> knownPlugins = new List<Guid>();
            foreach (Plugin plugin in setOfAllPlugins) {
                knownPlugins.Add(plugin.Id);
            }

            // Save known plugins in config.
            settings.KnownPlugins = knownPlugins;

            // Save icon files for default plugins. Icon files for pipeline plugins
            // are saved with the pipeline plugins, below.
            foreach (Plugin plugin in setOfAllPlugins) {
                if (plugin is DefaultPlugin) {
                    settings.SetIconFileOfPlugin(plugin.Id, plugin.IconFile);
                }
            }

            // Build list of pipeline plugins from the list of all plugins.
            List<PipelinePluginInfo> pipelinePluginInfos = new List<PipelinePluginInfo>();
            foreach (Plugin plugin in allPlugins) {
                if (plugin is PipelinePlugin) {
                    pipelinePluginInfos.Add((plugin as PipelinePlugin).Info);
                }
            }

            // Save pipeline plugins in config.
            settings.PipelinePlugins = pipelinePluginInfos;
            
            // Save options.
            if (AddQuotesChk.Checked != settings.AddQuotes) {
                settings.AddQuotes = AddQuotesChk.Checked;
            }
            if (EmailLinksChk.Checked != settings.MakeEmailLinks) {
                settings.MakeEmailLinks = EmailLinksChk.Checked;
            }
            if (HiddenSharesChk.Checked != settings.UseHiddenShares) {
                settings.UseHiddenShares = HiddenSharesChk.Checked;
            }
            if (AlwaysShowSubmenuChk.Checked != settings.AlwaysShowSubmenu) {
                settings.AlwaysShowSubmenu = AlwaysShowSubmenuChk.Checked;
            }
            if (UseIconForSubmenuChk.Checked != settings.UseIconForSubmenu) {
                settings.UseIconForSubmenu = UseIconForSubmenuChk.Checked;
            }
            if (UsePreviewModeChk.Checked != settings.UsePreviewMode) {
                settings.UsePreviewMode = UsePreviewModeChk.Checked;
            }
            if (DropRedundantWordsChk.Checked != settings.DropRedundantWords) {
                settings.DropRedundantWords = DropRedundantWordsChk.Checked;
            }
            if (EnableSoftwareUpdateChk.Checked != (!settings.DisableSoftwareUpdate)) {
                settings.DisableSoftwareUpdate = !EnableSoftwareUpdateChk.Checked;
            }

            // Encode param is a little special (see above)
            UserSettings.StringEncodeParam encodeParam;
            if (EncodeURIWhitespaceChk.Enabled && EncodeURIWhitespaceChk.Checked) {
                if (EncodeURICharsChk.Enabled && EncodeURICharsChk.Checked) {
                    encodeParam = UserSettings.StringEncodeParam.All;
                } else {
                    encodeParam = UserSettings.StringEncodeParam.Whitespace;
                }
            } else {
                encodeParam = UserSettings.StringEncodeParam.None;
            }
            if (encodeParam != settings.EncodeParam) {
                settings.EncodeParam = encodeParam;
            }

            // "Copy on same line" is a little special (see above)
            if (CopyOnSameLineChk.Enabled) {
                string pathsSeparator = CopyOnSameLineChk.Checked
                    ? PATHS_SEPARATOR_ON_SAME_LINE : String.Empty;
                if (pathsSeparator != settings.PathsSeparator) {
                    settings.PathsSeparator = pathsSeparator;
                }
            }

            // Now that everything is saved, disable the "Apply button".
            ApplyBtn.Enabled = false;
        }

        /// <summary>
        /// Updates the state of buttons on the Commands page.
        /// Call this when selection changes somewhere.
        /// </summary>
        private void UpdatePluginButtons()
        {
            MoveMainMenuPluginUpBtn.Enabled = MainMenuPluginsListView.SelectionCount == 1 &&
                MainMenuPluginsListView.SelectedIndex > 0;
            MoveMainMenuPluginDownBtn.Enabled = MainMenuPluginsListView.SelectionCount == 1 &&
                MainMenuPluginsListView.SelectedIndex >= 0 &&
                MainMenuPluginsListView.SelectedIndex < pluginsInMainMenu.Count - 1;
            AddPluginToMainMenuBtn.Enabled = AllPluginsListView.SelectionCount > 0;
            RemovePluginFromMainMenuBtn.Enabled = MainMenuPluginsListView.SelectionCount > 0;

            MoveSubmenuPluginUpBtn.Enabled = SubmenuPluginsListView.SelectionCount == 1 &&
                SubmenuPluginsListView.SelectedIndex > 0;
            MoveSubmenuPluginDownBtn.Enabled = SubmenuPluginsListView.SelectionCount == 1 &&
                SubmenuPluginsListView.SelectedIndex >= 0 &&
                SubmenuPluginsListView.SelectedIndex < pluginsInSubmenu.Count - 1;
            AddPluginToSubmenuBtn.Enabled = AllPluginsListView.SelectionCount > 0;
            RemovePluginFromSubmenuBtn.Enabled = SubmenuPluginsListView.SelectionCount > 0;

            NewPipelinePluginBtn.Enabled = true;
            EditPipelinePluginBtn.Enabled = DeletePipelinePluginBtn.Enabled =
                AllPluginsListView.SelectionCount == 1 && allPlugins[AllPluginsListView.SelectedIndex] is PipelinePlugin;
            ExportPipelinePluginsBtn.Enabled = false;
            if (AllPluginsListView.SelectionCount > 0) {
                ExportPipelinePluginsBtn.Enabled = true;
                foreach (var selectedPluginInfo in AllPluginsListView.Selection) {
                    if (!(selectedPluginInfo.Plugin is PipelinePlugin)) {
                        ExportPipelinePluginsBtn.Enabled = false;
                        break;
                    }
                }
            }
            ImportPipelinePluginsBtn.Enabled = true;
        }
        
        /// <summary>
        /// Called when the form is first loaded. We initialize controls here.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MainForm_Load(object sender, EventArgs e)
        {
            // Create user settings object and load settings in the form.
            settings = new UserSettings();
            LoadUserSettings();
        }
        
        /// <summary>
        /// Called when the form finally closes. We need to do cleanup work here.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            // Dispose of user settings object.
            if (settings != null) {
                settings.Dispose();
                settings = null;
            }
        }
        
        /// <summary>
        /// Called when the user clicks the "Apply" button. We'll need to save
        /// the user settings.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void ApplyBtn_Click(object sender, EventArgs e)
        {
            // Save user settings but do not close the form.
            SaveUserSettings();
        }
        
        /// <summary>
        /// Called when the user clicks the "Cancel" button. We close the form.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void CancelBtn_Click(object sender, EventArgs e)
        {
            // Simply close the form.
            Close();
        }
        
        /// <summary>
        /// Called when the user clicks the "OK" button. We save the user settings
        /// then close the form.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void OKBtn_Click(object sender, EventArgs e)
        {
            // Save user settings then close the form.
            SaveUserSettings();
            Close();
        }
        
        /// <summary>
        /// Called when the user checks or unchecks a checkbox in the Options tab.
        /// We'll enable the "Apply" button to allow the user to apply those changes.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void Chk_CheckedChanged(object sender, EventArgs e)
        {
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when the user checks or unchecks the "Encode whitespace"
        /// checkbox. We need to enable or disable the "Encode URI chars"
        /// checkbox when this occurs since it's conditional to the former.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void EncodeURIWhitespaceChk_CheckedChanged(object sender, EventArgs e)
        {
            EncodeURICharsChk.Enabled = EncodeURIWhitespaceChk.Checked;
        }
        
        /// <summary>
        /// Called when the user clicks on the link to visit the PCC website.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void CodePlexSiteLinkLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            // Link is the control's text.
            Process.Start(CodePlexSiteLinkLbl.Text);
        }
        
        /// <summary>
        /// Called when the user clicks on the link to see the license file.
        /// We need to locate the license file on disk and open it.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void LicenseTxtLinkLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            Debug.Assert(licensePath != null);

            Process.Start(licensePath);
        }
        
        /// <summary>
        /// Called when a link has been clicked related to the donations page.
        /// The URL to follow is in the link data.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void DonationLinkLbl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            Process.Start((string) e.Link.LinkData);
        }

        /// <summary>
        /// Called when selection changes in a plugins list view.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void AllPluginsListView_SelectionChanged(object sender, EventArgs e)
        {
            UpdatePluginButtons();
        }

        /// <summary>
        /// Called when the icon of a plugin is updated in a plugins list view.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void AllPluginsListView_PluginIconChanged(object sender, EventArgs e)
        {
            // Enable Apply button when a plugin's icon changes.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when the user presses the button to move a submenu plugin up one position.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MoveSubmenuPluginUpBtn_Click(object sender, EventArgs e)
        {
            // Move plugin up.
            int index = SubmenuPluginsListView.SelectedIndex;
            Debug.Assert(index > 0);
            Debug.Assert(index < pluginsInSubmenu.Count);
            Plugin plugin = pluginsInSubmenu[index];
            pluginsInSubmenu.RemoveAt(index);
            pluginsInSubmenu.Insert(index - 1, plugin);

            // Reselect same plugin.
            SubmenuPluginsListView.SelectedIndex = index - 1;

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when the user presses the button to move a submenu plugin down one position.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MoveSubmenuPluginDownBtn_Click(object sender, EventArgs e)
        {
            // Move plugin down.
            int index = SubmenuPluginsListView.SelectedIndex;
            Debug.Assert(index >= 0);
            Debug.Assert(index < pluginsInSubmenu.Count - 1);
            Plugin plugin = pluginsInSubmenu[index];
            pluginsInSubmenu.RemoveAt(index);
            pluginsInSubmenu.Insert(index + 1, plugin);

            // Reselect same plugin.
            SubmenuPluginsListView.SelectedIndex = index + 1;

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when user presses the button to add one or more plugins to the submenu.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void AddPluginToSubmenuBtn_Click(object sender, EventArgs e)
        {
            // Insert all plugins before current selection.
            int whereTo = SubmenuPluginsListView.LastSelectedIndex;
            if (whereTo >= 0) {
                foreach (var selectedPlugin in AllPluginsListView.Selection) {
                    pluginsInSubmenu.Insert(whereTo++, selectedPlugin.Plugin);
                }
            } else {
                foreach (var selectedPlugin in AllPluginsListView.Selection) {
                    pluginsInSubmenu.Add(selectedPlugin.Plugin);
                }
            }

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when user presses the button to remove one or more plugins from the submenu.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void RemovePluginFromSubmenuBtn_Click(object sender, EventArgs e)
        {
            List<int> indexes = new List<int>();
            foreach (var selectedPlugin in SubmenuPluginsListView.Selection) {
                indexes.Add(selectedPlugin.Index);
            }
            indexes.Sort((a, b) => b - a);
            foreach (int index in indexes) {
                pluginsInSubmenu.RemoveAt(index);
            }

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when the user presses the button to move a main menu plugin up one position.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MoveMainMenuPluginUpBtn_Click(object sender, EventArgs e)
        {
            // Move plugin up.
            int index = MainMenuPluginsListView.SelectedIndex;
            Debug.Assert(index > 0);
            Debug.Assert(index < pluginsInMainMenu.Count);
            Plugin plugin = pluginsInMainMenu[index];
            pluginsInMainMenu.RemoveAt(index);
            pluginsInMainMenu.Insert(index - 1, plugin);

            // Reselect same plugin.
            MainMenuPluginsListView.SelectedIndex = index - 1;

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when the user presses the button to move a main menu plugin down one position.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void MoveMainMenuPluginDownBtn_Click(object sender, EventArgs e)
        {
            // Move plugin down.
            int index = MainMenuPluginsListView.SelectedIndex;
            Debug.Assert(index >= 0);
            Debug.Assert(index < pluginsInMainMenu.Count - 1);
            Plugin plugin = pluginsInMainMenu[index];
            pluginsInMainMenu.RemoveAt(index);
            pluginsInMainMenu.Insert(index + 1, plugin);

            // Reselect same plugin.
            MainMenuPluginsListView.SelectedIndex = index + 1;

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when user presses the button to add one or more plugins to the main menu.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void AddPluginToMainMenuBtn_Click(object sender, EventArgs e)
        {
            // Insert all plugins before current selection.
            int whereTo = MainMenuPluginsListView.LastSelectedIndex;
            if (whereTo >= 0) {
                foreach (var selectedPlugin in AllPluginsListView.Selection) {
                    pluginsInMainMenu.Insert(whereTo++, selectedPlugin.Plugin);
                }
            } else {
                foreach (var selectedPlugin in AllPluginsListView.Selection) {
                    pluginsInMainMenu.Add(selectedPlugin.Plugin);
                }
            }

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when user presses the button to remove one or more plugins from the main menu.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void RemovePluginFromMainMenuBtn_Click(object sender, EventArgs e)
        {
            List<int> indexes = new List<int>();
            foreach (var selectedPlugin in MainMenuPluginsListView.Selection) {
                indexes.Add(selectedPlugin.Index);
            }
            indexes.Sort((a, b) => b - a);
            foreach (int index in indexes) {
                pluginsInMainMenu.RemoveAt(index);
            }

            // All this will enable the "Apply" button.
            ApplyBtn.Enabled = true;
        }

        /// <summary>
        /// Called when user presses the button to create a pipeline plugin.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void NewPipelinePluginBtn_Click(object sender, EventArgs e)
        {
            // Ask user to create a new plugin.
            using (PipelinePluginForm editForm = new PipelinePluginForm()) {
                PipelinePluginInfo pluginInfo = editForm.EditPlugin(this, settings, null);
                if (pluginInfo != null) {
                    // User pressed OK. Add the new plugin to the list view
                    // and select it.
                    Plugin plugin = pluginInfo.ToPlugin();
                    allPlugins.Add(plugin);
                    AllPluginsListView.SelectedIndex = allPlugins.Count - 1;

                    // Also add plugin to the submenu, which is probably the intent.
                    pluginsInSubmenu.Add(plugin);
                    SubmenuPluginsListView.SelectedIndex = pluginsInSubmenu.Count - 1;
                    
                    // All this will enable the "Apply" button.
                    ApplyBtn.Enabled = true;
                }
            }
        }

        /// <summary>
        /// Called when user presses the button to edit a pipeline plugin.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void EditPipelinePluginBtn_Click(object sender, EventArgs e)
        {
            // Get reference to the selected pipeline plugin.
            int pluginInfoIndex = AllPluginsListView.SelectedIndex;
            Debug.Assert(pluginInfoIndex >= 0);
            PipelinePluginInfo pluginInfo = ((PipelinePlugin) allPlugins[pluginInfoIndex]).Info;

            // Ask user to edit the plugin info.
            using (PipelinePluginForm editForm = new PipelinePluginForm()) {
                try {
                    PipelinePluginInfo newPluginInfo = editForm.EditPlugin(this, settings, pluginInfo);
                    if (newPluginInfo != null) {
                        // Replace the existing plugin object in the listbox.
                        Debug.Assert(newPluginInfo.Id == pluginInfo.Id);
                        Plugin oldPlugin = allPlugins[pluginInfoIndex];
                        Plugin newPlugin = newPluginInfo.ToPlugin();
                        allPlugins[pluginInfoIndex] = newPlugin;

                        // Also update the plugin in menus if it's found in either of them.
                        for (int i = 0; i < pluginsInMainMenu.Count; ++i) {
                            if (pluginsInMainMenu[i].Equals(oldPlugin)) {
                                pluginsInMainMenu[i] = newPlugin;
                            }
                        }
                        for (int i = 0; i < pluginsInSubmenu.Count; ++i) {
                            if (pluginsInSubmenu[i].Equals(oldPlugin)) {
                                pluginsInSubmenu[i] = newPlugin;
                            }
                        }
                        
                        // All this will enable the "Apply" button.
                        ApplyBtn.Enabled = true;
                    }
                } catch (InvalidPipelineException) {
                    // Maybe a plugin created by a newer version of Path Copy Copy?
                    MessageBox.Show(this, Resources.MainForm_Error_InvalidPipelinePluginEncodedElements,
                        Resources.MainForm_Confirm_EditMsgTitle, MessageBoxButtons.OK, MessageBoxIcon.Warning);
                }
            }
        }

        /// <summary>
        /// Called when user presses the button to remove a pipeline plugin.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void DeletePipelinePluginBtn_Click(object sender, EventArgs e)
        {
            // First confirm since this is an "irreversible" action. We have a string
            // in resources for this message and it includes a format placeholder
            // for the plugin description.
            int infoIndex = AllPluginsListView.SelectedIndex;
            PipelinePluginInfo info = ((PipelinePlugin) allPlugins[infoIndex]).Info;
            DialogResult res = MessageBox.Show(this, String.Format(Resources.REMOVE_PIPELINE_PLUGIN_MESSAGE, info.Description),
                Resources.REMOVE_PIPELINE_PLUGIN_TITLE, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
            if (res == DialogResult.OK) {
                // Remove plugin from menus if it's found in either of them.
                Plugin plugin = allPlugins[infoIndex];
                while (pluginsInMainMenu.Remove(plugin)) {
                }
                while (pluginsInSubmenu.Remove(plugin)) {
                }

                // Remove plugin from list view.
                allPlugins.RemoveAt(infoIndex);

                // All this will enable the "Apply" button.
                ApplyBtn.Enabled = true;
            }
        }

        /// <summary>
        /// Called when user presses the button to export pipeline plugins.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void ExportPipelinePluginsBtn_Click(object sender, EventArgs e)
        {
            // Ask user where to save the data.
            if (ExportPipelinePluginsSaveDlg.ShowDialog(this) == DialogResult.OK) {
                // Create a pipeline plugins collection and add all selected plugins to it.
                PipelinePluginCollection collection = new PipelinePluginCollection();
                foreach (var selectedPlugin in AllPluginsListView.Selection) {
                    collection.Plugins.Add(((PipelinePlugin) selectedPlugin.Plugin).Info);
                }

                // Open file where to save data and serialize collection in XML.
                // Use file extension to determine if we should use legacy serializing.
                using (FileStream fstream = new FileStream(ExportPipelinePluginsSaveDlg.FileName, FileMode.Create)) {
                    collection.ToXML(fstream, String.Equals(Path.GetExtension(
                        ExportPipelinePluginsSaveDlg.FileName), LEGACY_PIPELINE_PLUGINS_EXT,
                        StringComparison.CurrentCultureIgnoreCase));
                }
            }
        }

        /// <summary>
        /// Called when user presses the button to import pipeline plugins.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void ImportPipelinePluginsBtn_Click(object sender, EventArgs e)
        {
            // Ask user to select a file.
            if (ImportPipelinePluginsOpenDlg.ShowDialog(this) == DialogResult.OK) {
                // Open the file for reading and deserialize a collection of pipeline plugins.
                PipelinePluginCollection collection = null;
                using (FileStream fstream = new FileStream(ImportPipelinePluginsOpenDlg.FileName, FileMode.Open)) {
                    try {
                        collection = PipelinePluginCollection.FromXML(fstream);

                        // If this is a legacy file without required versions, compute them now.
                        if (String.Equals(Path.GetExtension(ImportPipelinePluginsOpenDlg.FileName),
                            LEGACY_PIPELINE_PLUGINS_EXT, StringComparison.CurrentCultureIgnoreCase)) {

                            foreach (PipelinePluginInfo info in collection.Plugins) {
                                try {
                                    Pipeline pipeline = PipelineDecoder.DecodePipeline(info.EncodedElements);
                                    info.RequiredVersion = pipeline.RequiredVersion;
                                } catch (InvalidPipelineException) {
                                    // Most illogical, Captain. Why can't we compute the required version
                                    // of something legacy? Regardless, there's not much we can do.
                                }
                            }
                        }
                    } catch (InvalidOperationException) {
                        Debug.Assert(collection == null);
                    }
                }

                // Make sure it worked.
                if (collection != null && collection.Plugins.Count > 0) {
                    // Go through list of plugins and locate any plugin overwrites.
                    ImportedPipelinePluginOverwrites pluginOverwrites = new ImportedPipelinePluginOverwrites();
                    for (int i = 0; i < allPlugins.Count; ++i) {
                        PipelinePluginInfo existingInfo = (allPlugins[i] as PipelinePlugin)?.Info;
                        if (existingInfo != null) {
                            PipelinePluginInfo newInfo = collection.Plugins.Find(pl => pl.Equals(existingInfo));
                            if (newInfo != null) {
                                pluginOverwrites.OverwriteInfos[newInfo] =
                                    new ImportedPipelinePluginOverwrites.OverwriteInfo(existingInfo, i);
                            }
                        }
                    }

                    // Popup a form asking the user which plugins to import.
                    bool shouldContinue;
                    using (ImportPipelinePluginsForm importForm = new ImportPipelinePluginsForm()) {
                        shouldContinue = importForm.SelectPlugins(this, ref collection, ref pluginOverwrites);
                    }
                    if (shouldContinue) {
                        // Check if any plugin to import will overwrite existing ones.
                        // If so, ask user to confirm overwrite.
                        if (pluginOverwrites.OverwriteInfos.Count > 0) {
                            shouldContinue = MessageBox.Show(this, Resources.MainForm_Confirm_ImportPipelinePluginOverwrite,
                                Resources.MainForm_Confirm_ImportMsgTitle, MessageBoxButtons.OKCancel,
                                MessageBoxIcon.Warning) == DialogResult.OK;
                        }
                        if (shouldContinue) {
                            // Reset all Global flags so that imported plugins are user-specific.
                            collection.Plugins.ForEach(pl => pl.Global = false);

                            // Scan plugins to import. If the plugin already exists, overwrite the existing
                            // one, otherwise add it at the end.
                            bool addedNewPlugins = false;
                            foreach (var newPluginInfo in collection.Plugins) {
                                Plugin newPlugin = newPluginInfo.ToPlugin();
                                ImportedPipelinePluginOverwrites.OverwriteInfo overwriteInfo;
                                if (pluginOverwrites.OverwriteInfos.TryGetValue(newPluginInfo, out overwriteInfo)) {
                                    Plugin oldPlugin = allPlugins[overwriteInfo.OldIndex];
                                    allPlugins[overwriteInfo.OldIndex] = newPlugin;

                                    // Also update it in menus if it's found in either of them.
                                    for (int i = 0; i < pluginsInMainMenu.Count; ++i) {
                                        if (pluginsInMainMenu[i].Equals(oldPlugin)) {
                                            pluginsInMainMenu[i] = newPlugin;
                                        }
                                    }
                                    for (int i = 0; i < pluginsInSubmenu.Count; ++i) {
                                        if (pluginsInSubmenu[i].Equals(oldPlugin)) {
                                            pluginsInSubmenu[i] = newPlugin;
                                        }
                                    }
                                } else {
                                    allPlugins.Add(newPlugin);
                                    addedNewPlugins = true;

                                    // Also add it to the submenu which is probably the intent.
                                    pluginsInSubmenu.Add(newPlugin);
                                }
                            }

                            // If we added new plugins, select the last one.
                            if (addedNewPlugins) {
                                AllPluginsListView.SelectedIndex = allPlugins.Count - 1;
                                SubmenuPluginsListView.SelectedIndex = pluginsInSubmenu.Count - 1;
                            }

                            // All this will enable the "Apply" button.
                            ApplyBtn.Enabled = true;
                        }
                    }
                } else if (collection == null) {
                    // Error deserializing the XML document for some reason.
                    MessageBox.Show(this, Resources.MainForm_Error_ImportPipelinePluginBadXMLFormat,
                        Resources.MainForm_Confirm_ImportMsgTitle, MessageBoxButtons.OK, MessageBoxIcon.Error);
                } else {
                    Debug.Assert(collection.Plugins.Count == 0);
                    // Empty XML document for some reason.
                    MessageBox.Show(this, Resources.MainForm_Error_ImportPipelinePluginEmptyXML,
                        Resources.MainForm_Confirm_ImportMsgTitle, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        /// <summary>
        /// Called when user presses the button to export user settings.
        /// </summary>
        /// <param name="sender">Event sender.</param>
        /// <param name="e">Event arguments.</param>
        private void ExportUserSettingsBtn_Click(object sender, EventArgs e)
        {
            // In order to export complete settings, we need to apply changes if needed.
            bool canExport = true;
            if (ApplyBtn.Enabled) {
                canExport = MessageBox.Show(this, Resources.MainForm_Confirm_ExportUserSettingsSaving,
                    Resources.MainForm_Confirm_ExportUserSettingsMsgTitle, MessageBoxButtons.YesNo,
                    MessageBoxIcon.Question) == DialogResult.Yes;
                if (canExport) {
                    SaveUserSettings();
                }
            }
            if (canExport && ExportUserSettingsSaveDlg.ShowDialog(this) == DialogResult.OK) {
                using (new CursorChanger(this, Cursors.WaitCursor)) {
                    UserSettings.ExportUserSettings(ExportUserSettingsSaveDlg.FileName);
                }
                MessageBox.Show(this, String.Format(Resources.MainForm_Msg_UserSettingsExported,
                    ExportUserSettingsSaveDlg.FileName), Resources.MainForm_Msg_UserSettingsExportedMsgTitle,
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }
    }
}
